package moe.server;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import moe.config.Configuration;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicHeaderElement;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;

public class HttpSessionInterceptor implements HttpRequestInterceptor {
	private static final Logger log = Logger.getLogger(HttpSessionInterceptor.class.getName());

	private static final String COOKIE_HEADER = "Cookie";
	private static final String SET_COOKIE_HEADER = "Set-Cookie";

	private static final String SESSION_ID = "moe_session_id";
	private static final int COOKIE_TIMEOUT = Configuration.getSessionExpires();
	private static final long SESSION_TIMEOUT = COOKIE_TIMEOUT * 60 * 1000;
	private static final long RECOVER_INTERVAL = SESSION_TIMEOUT / 3;

	private static final int GC_THRESHOLD = 12;

	private Map<String, HttpSession> sessionMap;

	private SimpleDateFormat dateFormat;
	private Thread sessionRecoverThread;

	public HttpSessionInterceptor() {
		dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
		dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));

		sessionMap = new ConcurrentHashMap<String, HttpSession>();

		if (isUseServerCache()) {
			runSessionRecoverThread();
		}
	}

	@Override
	protected void finalize() {
		if (sessionRecoverThread != null && !sessionRecoverThread.isInterrupted()) {
			sessionRecoverThread.interrupt();
		}
	}

	private void runSessionRecoverThread() {
		sessionRecoverThread = new Thread("SessionRecoverThread") {
			@Override
			public void run() {
				while (true) {
					try {
						sleep(RECOVER_INTERVAL);
					} catch (InterruptedException e) {
						return;
					}
					doRecover();
				}
			}
		};
		sessionRecoverThread.setDaemon(true);
		sessionRecoverThread.start();
	}

	private void doRecover() {
		Collection<HttpSession> sessions = sessionMap.values();
		boolean hasGarbage = false;
		for (HttpSession session : sessions) {
			if (session.isStale()) {
				if (log.isLoggable(Level.FINE)) {
					log.fine("session timeout, id:" + session.getId());
				}
				session.invalidate();
				sessions.remove(session);
				hasGarbage = true;
			}
		}

		if (hasGarbage) {
			System.gc();
		} else if (sessionMap.isEmpty()) {
			Runtime rt = Runtime.getRuntime();
			long total = rt.totalMemory(), max = rt.maxMemory();
			if (max / total < GC_THRESHOLD) {
				if (log.isLoggable(Level.FINE)) {
					log.fine("Memory max/total(" + max + "/" + total + ") underflow GC_THRESHOLD("
							+ GC_THRESHOLD + "), clear heap");
				}
				System.gc();
			}
		}
	}

	public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
		HttpSession session = getSession(request);
		if (session == null && available(request)) {
			session = createSession(context);
			if (log.isLoggable(Level.FINE)) {
				log.fine("request: \"" + request.getRequestLine()
						+ "\" create session , session id: "
						+ session.getId());
			}
		}
		setSession(session, context);
	}

	private boolean available(HttpRequest request) {
		String uri = request.getRequestLine().getUri();
		return uri.startsWith("/json") && isUseServerCache();
	}

	private boolean isUseServerCache() {
		return Configuration.getServerCache() > 0;
	}

	private HttpSession getSession(HttpRequest request) {
		HttpSession session = null;
		String sessionId = getSessionId(request);
		if (sessionId != null) {
			session = sessionMap.get(sessionId);
			if (session != null && session.isStale()) {
				session.invalidate();
				session = null;
				sessionMap.remove(sessionId);
			}
		}
		return session;
	}

	private void setSession(HttpSession session, HttpContext context) {
		context.setAttribute(HttpSession.SESSION_CONTEXT, session);
		activateSession(session, context);
	}

	private HttpSession createSession(HttpContext context) {
		String sessionId = setSessionId(context);
		HttpSession session = new HttpSession(sessionId, SESSION_TIMEOUT);
		sessionMap.put(sessionId, session);
		return session;
	}

	private String getSessionId(HttpRequest request) {
		String sessionId = null;
		if (request.containsHeader(COOKIE_HEADER)) {
			Header header = request.getFirstHeader(COOKIE_HEADER);
			if (header != null) {
				HeaderElement[] elements = header.getElements();
				if (elements.length > 0) {
					for (HeaderElement element : elements) {
						if (SESSION_ID.equals(element.getName())) {
							sessionId = element.getValue();
						}
					}
				}
			}
		}
		return sessionId;
	}

	private String setSessionId(HttpContext context) {
		UUID uuid = UUID.randomUUID();
		String sessionId = uuid.toString();
		updateCookie(context, sessionId);
		return sessionId;
	}

	private void updateCookie(HttpContext context, String sessionId) {
		Date date = new Date();
		date.setMinutes(date.getMinutes() + COOKIE_TIMEOUT);
		NameValuePair[] nvps =
				new NameValuePair[] { new BasicNameValuePair("expires", dateFormat.format(date)),
						new BasicNameValuePair("path", "/") };

		BasicHeaderElement cookie = new BasicHeaderElement(SESSION_ID, sessionId, nvps);
		HttpResponse response = (HttpResponse) context.getAttribute(ExecutionContext.HTTP_RESPONSE);
		response.addHeader(SET_COOKIE_HEADER, cookie.toString());
	}

	private void activateSession(HttpSession session, HttpContext context) {
		if (session != null) {
			session.activate();
			updateCookie(context, session.getId());
		}
	}

}
